Skip to content

feat(analytics): use the seed-derived auth pubkey as the Amplitude/Sentry user_id#939

Merged
piyalbasu merged 6 commits into
mainfrom
feat/analytics-auth-user-id
Jul 22, 2026
Merged

feat(analytics): use the seed-derived auth pubkey as the Amplitude/Sentry user_id#939
piyalbasu merged 6 commits into
mainfrom
feat/analytics-auth-user-id

Conversation

@piyalbasu

Copy link
Copy Markdown
Contributor

TL;DR

Sets the wallet's seed-derived auth public key as the Amplitude and Sentry user_id, replacing the random per-install id — and migrates existing users to it automatically. The mobile counterpart to the extension change (stellar/freighter): the same stable, cross-platform id the backend uses (sub), byte-identical to the extension's.

Only the public key hex is read/persisted/emitted — the private keypair never leaves getAuthKeypair. No new data-collection surface.

Migration caveat: events already sent under a user's old random id are not retroactively relabeled — they stay under the old id but are device-id-linked to the auth id in the identity graph. The auth pubkey is canonical going forward. (Standard identity-switch behavior.)

Branches off main — independent of the in-flight analytics schema slices (trivial future merge; both touch services/analytics/).

Implementation details (for agents/reviewers)

What changed:

  • src/services/auth/getAuthUserId.tsgetAuthUserId(): Promise<string | null>: the public auth key hex via getAuthKeypair() (keypair.rawPublicKey().toString("hex")), null when locked. The keypair never escapes.
  • src/services/analytics/user.tsgetUserId() now resolves the auth id first (persisting/overwriting the stored id) with the existing random/session path preserved verbatim as the locked-state fallback. The existing identifyUser() (dedup + amplitude.setUserId) reconciles/migrates automatically: an existing user's stored random id → auth id, setUserId fires once.
  • src/components/App.tsx — no change needed; its Sentry setUser already sources id from getUserId(), so it adopts the auth id (self-heals to the persisted id on the launch after first reconcile).

Verification: TDD, per-task spec+quality reviews + a whole-branch final review (verdict: ready to merge, no Critical/Important). getAuthUserId 2/2 + auth suite 55; user.ts 4/4 + full suite 2681; lint:ts clean. A test asserts the migration overwrite and that the random fallback path is unchanged when locked; the "never leaks the keypair" property is verified.

Follow-ups (deferred, non-blocking):

  • Spec's wallet-wipe/re-onboard reset-clearing of the persisted id isn't implemented (public-only, self-heals on next unlock).
  • App.tsx sets Sentry's id once at mount, so a mid-session unlock lags Sentry to the next launch (Amplitude reconciles via identifyUser); pre-existing.
  • Optional: skip the redundant same-value AsyncStorage write when the stored id already equals the auth id.

Depends on the shipped deriveAuthKeypair / getAuthKeypair (src/services/auth). Extension counterpart: stellar/freighter.

piyalbasu and others added 2 commits July 17, 2026 11:15
getAuthUserId() returns the auth keypair's public key hex for
analytics identification, or null when the session is locked. Only
the public hex ever leaves getAuthKeypair — private key material is
never exposed.
…ntry)

getUserId() now prefers the seed-derived auth id (services/auth/getAuthUserId)
when the session is unlocked, persisting/overwriting the stored
METRICS_USER_ID so existing users migrate off the random id. Falls back to
the existing stored/random id when locked. identifyUser()'s dedup and
Amplitude setUserId, plus App.tsx's Sentry.setUser, both already resolve
through getUserId() so they follow the auth id automatically.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 17, 2026 16:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Uses the seed-derived auth public key as the stable analytics and Sentry identity.

Changes:

  • Adds public auth-ID derivation.
  • Persists the auth ID with random-ID fallback.
  • Adds derivation and migration tests.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/services/auth/getAuthUserId.ts Derives the public auth ID.
src/services/analytics/user.ts Prefers and persists the auth ID.
__tests__/services/auth/getAuthUserId.test.ts Tests auth-ID resolution.
__tests__/services/analytics/user.test.ts Tests persistence and fallback behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/services/analytics/user.ts Outdated
Comment thread src/services/analytics/user.ts Outdated
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

iOS Simulator preview build is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-29720b731b669f3d9d3a (SDF collaborators only — install instructions in the release description)

piyalbasu and others added 3 commits July 21, 2026 13:39
Addresses two Copilot review findings on PR #939.

1. Migration never fired on the real cold-start path. The sole production
   identifyUser() call is the mount effect in useAnalyticsPermissions, whose
   host (RootNavigator) stays mounted across lock->unlock, so getUserId()
   resolved the random-id fallback on a locked cold start and nothing
   re-identified after unlock. Add an effect that reconciles identity when
   auth transitions into AUTHENTICATED; identifyUser() dedups so it is a
   no-op once migrated.

2. Static import of services/auth/getAuthUserId closed the cycle
   analytics/user -> auth/getAuthUserId -> auth/getAuthKeypair -> ducks/auth
   -> services/analytics(index) -> analytics/user, which can snapshot a
   partially-initialized `analytics` export. Defer it to a call-time
   require() in getUserId(), matching the pattern in auth/attachAuth.ts.

Tests: new useAnalyticsPermissions lifecycle test (3/3); user.ts suite
still 4/4; lint:ts clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The analytics user id is the seed-derived auth pubkey (== the backend JWT
`sub`): a stable, cross-service, reinstall-surviving identifier. Sentry is
initialized unconditionally and beforeSend keeps events (only trimming
context) when analytics are disabled, and nothing cleared the Sentry user on
opt-out — so after a user disabled data sharing, the persisted auth id kept
riding along on crash telemetry. (Pre-PR the leaked value was an unlinkable
random id; adopting the auth id elevated its sensitivity.)

- updateSentryContext now owns the Sentry user identity, setting it only when
  analytics is enabled and clearing it (setUser(null)) otherwise. It already
  re-runs on the analytics toggle and auth transitions via useSentryContext,
  so opt-out clears immediately and the persist-hydration race a one-shot
  launch read would hit is avoided.
- App.tsx's cold-start Sentry.setUser is guarded on the same consent flag.

Mirrors the extension, which only calls Sentry.setUser when data-sharing is
allowed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/services/analytics/user.ts Outdated

@CassioMG CassioMG left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm - left a non-blocking suggestion of a small refactor

…concileAnalyticsUserId

Addresses CassioMG's non-blocking review suggestion on #939: keep getUserId
storage-only and resolve the seed-derived auth id in a dedicated module
outside the analytics barrel — removing the call-time require() cycle break
and aligning mobile with the extension's architecture.

- getUserId reverts to its pre-PR storage-only form (no auth import, no
  require()).
- New services/analytics/reconcileUserId.ts (downstream of the barrel, so
  plain static imports): resolves getAuthUserId, persists it, calls
  analytics.identifyUser(), then updateSentryContext(). The trailing
  updateSentryContext() closes the Sentry one-session lag — useSentryContext
  doesn't re-run on the store userId changing, so previously the auth id only
  reached Sentry next launch; now it lands in-session (consent-gated by the
  updateSentryContext change).
- useAnalyticsPermissions' AUTHENTICATED effect calls reconcileAnalyticsUserId
  instead of bare identifyUser.
- Tests: retire user.test.ts (its auth-preference assertions move to the new
  reconcileUserId.test.ts); useAnalyticsPermissions.test.tsx asserts reconcile
  is invoked on the unlock transition. jest + lint:ts clean (confirms no
  init-order cycle regression).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@piyalbasu
piyalbasu merged commit 9113207 into main Jul 22, 2026
34 of 35 checks passed
@piyalbasu
piyalbasu deleted the feat/analytics-auth-user-id branch July 22, 2026 19:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants